import paho.mqtt.client as mqtt
import ssl
import base64
import asn1tools
import json
import time
import threading
import random
import datetime
import re
import redis
import pywraps2 as s2
from cacheout import Cache
import geofence as g

re_cam = r'.*\/cam\/(.*)'
re_denm = r'.*\/denm\/(.*)'
re_vin = r'(.*):.*'
start_timestamp = int(datetime.datetime(2004,1,1).timestamp()*1000)
denm_default_duration = 30
denm_radius = 200.0  #200 meters of radius
denm_degree = denm_radius/(40076020/2)*180
#geofence id and s2 cap relation in local cache
local_geofence_cache = Cache(maxsize=256, ttl=0)
#Vin and S2Point relation in local cache
local_vin_point_cache = Cache(maxsize=256, ttl=1)

geofence_cell_level = 12
geofence_cell_level_bits = geofence_cell_level*2+3
r = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)

def handle_cam(vin, payload):
    cam_msg = cam_asn.decode('CAM', base64.b64decode(payload))
    position = cam_msg['cam']['camParameters']['basicContainer']['referencePosition']
    lat = float(position['latitude'])/10000000
    lon = float(position['longitude'])/10000000
    # Convert the vehicle location to S2Point and store in local cache
    point = s2.S2LatLng_FromDegrees(lat, lon).ToPoint()
    cell = s2.S2CellId(point)
    local_vin_point_cache.set(vin, point, ttl=1)
    # Get the level30 cell id of the point and store in redis
    cell_id = cell.id()
    r.zadd('vin_loc', {vin:cell_id})
    r.set(vin, cell_id, ex=1)
    # Get the level12 cell id of the point
    parent_cell_id = cell.parent(geofence_cell_level).id()
    # Get the geofences that relate to the point parent cell
    geofences_parent_cell = r.smembers(parent_cell_id)
    # Check the previous saved vehicle and geofence relation
    geofences_previous = r.smembers('gf_'+vin)
    # If vin previous and current not in any geofences, do nothing
    if len(geofences_previous)==0 and len(geofences_parent_cell)==0:
        return
    elif len(geofences_parent_cell)>0:
        for geofence_id in geofences_parent_cell:
            s2_geofence = local_geofence_cache.get(geofence_id)
            # If not found, maybe it's not sync to local cacheout cache or expire
            if not s2_geofence:
                geofence_meta = r.get(geofence_id)
                # It means not sync to local cache, so create it
                if geofence_meta:
                    geofence_json = json.loads(geofence_meta)
                    s2_geofence = g.createCircleGeoFence(
                        geofence_json['lat'], 
                        geofence_json['lon'], 
                        geofence_json['denm_degree'])
                    local_geofence_cache.set(
                        geofence_id, 
                        s2_geofence, 
                        ttl=r.ttl(geofence_id))
            if s2_geofence:
                if s2_geofence.Contains(point):
                    if geofence_id not in geofences_previous:
                        vinInGFId = re.search(geofence_id)
                        if vinInGFId:
                            if vinInGFId.group(1) != vin:
                                print('VIN %s Lat:%f Lon:%f enters a geofence %s'%(vin, lat, lon, geofence_id))
                                # Store the vin and geofence relation in redis
                                r.sadd('gf_'+vin, geofence_id)
                else:
                    if geofence_id in geofences_previous:
                        print('VIN %s Lat:%f Lon:%f leaves a geofence %s'%(vin, lat, lon, geofence_id))
                        # Remove the vin and geofence relation in redis
                        r.srem('gf_'+vin, geofence_id)
            # Not found the geofence s2 instance as expire, remove it in redis
            else:
                print('Geofence %s removed'%geofence_id)
                r.srem(parent_cell_id, geofence_id)
    else:
        for geofence_id in geofences_previous:
            print('VIN %s Lat:%f Lon:%f leaves a geofence %s'%(vin, lat, lon, geofence_id))
            # Remove the vin and geofence relation in redis
            r.srem('gf_'+vin, geofence_id)

def handle_denm(vin, payload):
    denm_msg = denm_asn.decode('DENM', base64.b64decode(payload))
    print(denm_msg)
    denm_management = denm_msg['denm']['management']
    original_id = denm_management['actionID']['originatingStationID']
    sequence_num = denm_management['actionID']['sequenceNumber']
    detection_time = denm_management['detectionTime']
    validity_duration = denm_default_duration
    if 'validityDuration' in denm_management:
        validity_duration = denm_management['validityDuration']
    # Check if the denm message expire
    current_timestamp = datetime.datetime.now(datetime.timezone.utc).timestamp()*1000
    if current_timestamp > (start_timestamp + detection_time + validity_duration*1000):
        return
    position = denm_management['eventPosition']
    lat = float(position['latitude'])/10000000
    lon = float(position['longitude'])/10000000
    geofence_id = vin+':'+str(original_id)+'_'+str(sequence_num)
    # Check if the geofence already exist in local cache
    geofence_denm = local_geofence_cache.get(geofence_id)
    if not geofence_denm:
        # If not exit, check if it exist in redis cache
        geofence_meta = r.get(geofence_id)
        if not geofence_meta:
            print(payload)
            geofence_meta = json.dumps(
                {
                    'lat':lat, 
                    'lon':lon, 
                    'degree':denm_degree, 
                    'type':'circle', 
                    'message': str(payload, 'utf-8')
                }
            )
            r.set(geofence_id, geofence_meta, ex=validity_duration, nx=True)
        # Create it in the local cache 
        geofence_denm = g.createCircleGeoFence(lat, lon, denm_degree)
        print('create denm geofence id:%s center:(%f,%f)'%(geofence_id, lat, lon))
        local_geofence_cache.set(
            geofence_id, 
            geofence_denm, 
            ttl=r.ttl(geofence_id))
    # Get the default level 12 cell id covered by geofence
    cells_id = g.getGeoFenceCellId(geofence_denm, geofence_cell_level)
    # Store the vin that found within the geofence
    vin_in_geofence = []
    for c_id in cells_id:
        # Save the cellid and Geofence relation in redis
        r.sadd(c_id, geofence_id)
        cell_begin, cell_end = g.getChildCellIdRange(c_id)
        # Get the vehicles which level 30 cell id covered by the geofence level 12 cell
        # The redis key 'vin_loc' stores the vin and level 30 cell id relation
        vin_list = r.zrangebyscore('vin_loc', cell_begin, cell_end, withscores=True, score_cast_func=float)
        for vin_item in vin_list:
            if vin_item[0] != vin:
                point = local_vin_point_cache.get(vin_item[0])
                if not point:
                    if r.get(vin_item[0]):
                        point = s2.S2CellId(int(vin_item[1])).ToPoint()
                    else:
                        # The vin S2Point instance could not found as expires 1s.
                        r.zrem('vin_loc', vin_item[0])
                if point:
                    if geofence_denm.Contains(point):
                        r.sadd('gf_'+vin_item[0], geofence_id)
                        vin_in_geofence.append(vin_item[0])
    if len(vin_in_geofence)>0:
        print('Found vin in geofence:%s'%(','.join(vin_in_geofence)))
        for vin_notify in vin_in_geofence:
            client.publish("vehicle/"+vin_notify+"/denm",payload)

def on_connect(client, userdata, flags, rc):
    print("Connected with result code "+str(rc))

def on_message(client, userdata, msg):
    #print(msg.topic+" "+str(msg.payload))
    cam_topic = re.search(re_cam, msg.topic)
    if cam_topic:
        vin = cam_topic.group(1)
        handle_cam(vin, msg.payload)
    else:
        denm_topic = re.search(re_denm, msg.topic)
        if denm_topic:
            vin = denm_topic.group(1)
            handle_denm(vin, msg.payload)


client = mqtt.Client()
client.on_connect = on_connect
client.on_message = on_message
client.tls_set(ca_certs="cert/root.crt", certfile="cert/v2xapp.crt", keyfile="cert/v2xapp_key.pem", 
    cert_reqs=ssl.CERT_REQUIRED, tls_version=ssl.PROTOCOL_TLSv1_2, ciphers=None)

client.connect("broker", 8883, 60)

# Define the asn.1 format to decode
cam_asn = asn1tools.compile_files('CAM.asn', 'uper')
denm_asn = asn1tools.compile_files('DENM.asn', 'uper')

client.subscribe('vehicle/#', 0)
client.loop_start()

start = time.time()
while True:
    time.sleep(1)
    end = time.time()
    if int(end - start) > 30:
        client.loop_stop()
        break
client.disconnect()